<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SDP Compression Tool</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
            line-height: 1.6;
            color: #333;
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
        }
        .container {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
        textarea {
            width: 100%;
            height: 200px;
            padding: 10px;
            box-sizing: border-box;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-family: monospace;
            margin-bottom: 10px;
        }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-right: 10px;
            margin-bottom: 10px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #45a049;
        }
        .qrcode-container {
            margin-top: 20px;
            text-align: center;
        }
        .output {
            margin-top: 20px;
            width: 100%;
            height: max(100px, calc(100vh - 400px));
            overflow-y: auto;
            background-color: #f5f5f5;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-family: monospace;
            white-space: pre-wrap;
        }
        h3 {
            margin-top: 20px;
            margin-bottom: 10px;
        }
        .stats {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            margin-bottom: 20px;
        }
        .stat-box {
            background-color: #f0f0f0;
            padding: 15px;
            border-radius: 4px;
            flex: 1;
            min-width: 200px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
        }
        .input-group {
            margin-bottom: 15px;
        }
        .tabs {
            display: flex;
            border-bottom: 1px solid #ddd;
            margin-bottom: 20px;
        }
        .tab {
            padding: 10px 20px;
            cursor: pointer;
            background-color: #f8f8f8;
            border: 1px solid #ddd;
            border-bottom: none;
            margin-right: 5px;
            border-radius: 4px 4px 0 0;
        }
        .tab.active {
            background-color: white;
            border-bottom: 1px solid white;
            margin-bottom: -1px;
        }
        .tab-content {
            display: none;
        }
        .tab-content.active {
            display: block;
        }
    </style>
</head>
<body>
    <h1>SDP Compression Tool</h1>
    
    <div class="tabs">
        <div class="tab active" data-tab="compression">Compression</div>
        <div class="tab" data-tab="testing">Testing</div>
        <div class="tab" data-tab="ice">ICE Candidates</div>
    </div>

    <div id="compression" class="tab-content active">
        <h2>SDP Compression/Decompression</h2>
        
        <div class="container">
            <div>
                <h3>Input SDP</h3>
                <textarea id="sdpInput" placeholder="Paste SDP here..."></textarea>
                <button id="compressBtn">Compress</button>
                <button id="compressQRBtn">Compress for QR</button>
                <button id="generateSampleSDPBtn">Generate Sample SDP</button>
            </div>
            
            <div>
                <h3>Compressed Output</h3>
                <textarea id="compressedOutput" placeholder="Compressed SDP will appear here..."></textarea>
                <button id="decompressBtn">Decompress</button>
                <button id="decompressQRBtn">Decompress QR Format</button>
                <button id="copyCompressedBtn">Copy</button>
            </div>
        </div>
        
        <div class="stats">
            <div class="stat-box">
                <strong>Original Size:</strong> <span id="originalSize">0</span> bytes
            </div>
            <div class="stat-box">
                <strong>Compressed Size:</strong> <span id="compressedSize">0</span> bytes
            </div>
            <div class="stat-box">
                <strong>Compression Ratio:</strong> <span id="compressionRatio">0</span>%
            </div>
        </div>
        
        <div class="qrcode-container">
            <h3>QR Code</h3>
            <div id="qrcode"></div>
        </div>
        
        <h3>Decompressed Result</h3>
        <textarea id="decompressedOutput" placeholder="Decompressed SDP will appear here..."></textarea>
    </div>

    <div id="testing" class="tab-content">
        <h2>SDP Compression Testing</h2>
        <button id="runTestsBtn">Run All Tests</button>
        <button id="validateSDPBtn">Validate Current SDP</button>
        <h3>Test Output</h3>
        <div id="testOutput" class="output"></div>
    </div>

    <div id="ice" class="tab-content">
        <h2>ICE Candidates Compression</h2>
        <div class="input-group">
            <h3>ICE Candidates (JSON Array)</h3>
            <textarea id="iceInput" placeholder='[{"candidate":"candidate:0 1 UDP 2122260223 192.168.1.2 56789 typ host","sdpMid":"0","sdpMLineIndex":0}]'></textarea>
            <button id="compressIceBtn">Compress ICE</button>
            <button id="generateSampleIceBtn">Generate Sample ICE</button>
        </div>
        
        <div class="input-group">
            <h3>Compressed ICE</h3>
            <textarea id="iceCompressedOutput" placeholder="Compressed ICE candidates will appear here..."></textarea>
            <button id="decompressIceBtn">Decompress ICE</button>
            <button id="copyCompressedIceBtn">Copy</button>
        </div>
        
        <div class="input-group">
            <h3>Decompressed ICE</h3>
            <textarea id="iceDecompressedOutput" placeholder="Decompressed ICE candidates will appear here..."></textarea>
        </div>
    </div>
    <script>

function compressSDP(sdp) {
	const iceUfrag = sdp.match(/a=ice-ufrag:([^\r\n]+)/)[1];
	const icePwd = sdp.match(/a=ice-pwd:([^\r\n]+)/)[1];
	const fingerprintMatch = sdp.match(/a=fingerprint:sha-256 ([^\r\n]+)/);
	const fingerprint = fingerprintMatch[1].replace(/:/g, '');
	const isOffer = sdp.match(/a=setup:actpass/) !== null;
	const candidates = [];
	const candidateRegex = /a=candidate:([^\r\n]+)/g;
	let candidateMatch;
	while ((candidateMatch = candidateRegex.exec(sdp)) !== null) {
		const parts = candidateMatch[1].split(' ');
		if (parts[4].includes(':')) continue;
		const foundation = parts[0];
		const component = parts[1];
		const protocol = parts[2].toLowerCase() === 'udp' ? 'u' : 't';
		const priority = parseInt(parts[3]);
		const ip = parts[4];
		const port = parseInt(parts[5]);
		const type = parts[7];
		let typeCode;
		switch (type) {
			case 'host':
				typeCode = 'h';
				break;
			case 'srflx':
				typeCode = 's';
				break;
			case 'relay':
				typeCode = 'r';
				break;
			default:
				typeCode = 'x';
		}
		let relatedIP = '';
		let relatedPort = '';
		const rAddrIndex = parts.indexOf('raddr');
		if (rAddrIndex !== -1 && rAddrIndex + 1 < parts.length) {
			relatedIP = parts[rAddrIndex + 1];
		}
		const rPortIndex = parts.indexOf('rport');
		if (rPortIndex !== -1 && rPortIndex + 1 < parts.length) {
			relatedPort = parts[rPortIndex + 1];
		}
		let candidateStr = `${foundation},${component},${protocol},${priorityToCompact(priority)},${ip},${port},${typeCode}`;
		if (relatedIP && relatedPort) {
			candidateStr += `,${relatedIP},${relatedPort}`;
		}
		candidates.push(candidateStr);
	}
	const compactFingerprint = hexToCompact(fingerprint);
	let result = `C1|${isOffer ? 'o' : 'a'}|${iceUfrag}|${icePwd}|${compactFingerprint}`;
	if (candidates.length > 0) {
		result += `|${candidates.join('/')}`;
	}
	return result;
}

function decompressSDP(compressed) {
	try {
		const parts = compressed.split('|');
		if (parts[0] !== 'C1') {
			throw new Error('Unsupported compression version');
		}
		const isOffer = parts[1] === 'o';
		const iceUfrag = parts[2];
		const icePwd = parts[3];
		const fingerprint = compactToHex(parts[4]);
		// Format fingerprint correctly with colons every 2 characters
		const formattedFingerprint = fingerprint.match(/.{2}/g).join(':').toUpperCase();

		// Build the basic SDP structure
		let sdpLines = [
			'v=0',
			'o=- ' + Math.floor(Date.now()) + ' 2 IN IP4 127.0.0.1',
			's=-',
			't=0 0',
			'a=group:BUNDLE 0',
			'a=extmap-allow-mixed',
			'a=msid-semantic: WMS',
			'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
			'c=IN IP4 0.0.0.0',
			`a=ice-ufrag:${iceUfrag}`,
			`a=ice-pwd:${icePwd}`,
			'a=ice-options:trickle',
			`a=fingerprint:sha-256 ${formattedFingerprint}`,
			`a=setup:${isOffer ? 'actpass' : 'active'}`,
			'a=mid:0',
			'a=sctp-port:5000',
			'a=max-message-size:262144'
		];

		// Process candidates
		if (parts.length > 5 && parts[5]) {
			const candidates = parts[5].split('/');

			for (const candidate of candidates) {
				const candParts = candidate.split(',');
				if (candParts.length < 7) continue;

				// Extract candidate parts
				const foundation = candParts[0];
				const component = candParts[1];
				const protocol = candParts[2] === 'u' ? 'udp' : 'tcp'; // Use lowercase protocol
				const priority = compactToPriority(candParts[3]);
				const ip = candParts[4];
				const port = candParts[5];
				let type;
				switch (candParts[6]) {
					case 'h':
						type = 'host';
						break;
					case 's':
						type = 'srflx';
						break;
					case 'r':
						type = 'relay';
						break;
					default:
						type = 'unknown';
				}

				// Build candidate line
				let candidateLine = `a=candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`;

				// Add raddr/rport if available for srflx and relay candidates
				if (type === 'srflx' || type === 'relay') {
					// If related IP and port are specified in compression, use them
					if (candParts.length >= 9 && candParts[7]) {
						candidateLine += ` raddr ${candParts[7]} rport ${candParts[8]}`;
					} else {
						// Otherwise add defaults for Chrome compatibility
						candidateLine += ` raddr 0.0.0.0 rport 0`;
					}
				}

				// Add TCP type if needed
				if (protocol === 'tcp') {
					candidateLine += ' tcptype active';
				}

				// Add generation and network-cost
				candidateLine += ' generation 0 network-cost 999';

				sdpLines.push(candidateLine);
			}
		}

		// Join with proper line endings
		return sdpLines.join('\r\n') + '\r\n';
	} catch (e) {
		console.error("Error in decompressSDP:", e);
		throw e;
	}
}


function compressIP(ip) {
	if (ip === '127.0.0.1') return 'L';
	if (ip === '0.0.0.0') return 'Z';
	if (ip.startsWith('192.168.')) return 'P' + ip.split('.').slice(2).join('');
	if (ip.startsWith('10.')) return 'T' + ip.split('.').slice(1).join('');
	if (ip.startsWith('172.')) return 'S' + ip.split('.').slice(1).join('');
	const parts = ip.split('.');
	let num = 0;
	for (let i = 0; i < 4; i++) {
		num = num * 256 + parseInt(parts[i]);
	}
	return num.toString(36).toUpperCase();
}

function decompressIP(compressed) {
	if (compressed === 'L') return '127.0.0.1';
	if (compressed === 'Z') return '0.0.0.0';
	if (compressed.startsWith('P')) {
		const suffix = compressed.substring(1);
		if (suffix.length === 0) return '192.168.0.0';
		if (suffix.length === 1) return `192.168.${suffix}.0`;
		return `192.168.${suffix.substring(0, 1)}.${suffix.substring(1)}`;
	}
	if (compressed.startsWith('T')) {
		const suffix = compressed.substring(1);
		if (suffix.length === 0) return '10.0.0.0';
		if (suffix.length === 1) return `10.${suffix}.0.0`;
		if (suffix.length === 2) return `10.${suffix.substring(0, 1)}.${suffix.substring(1)}.0`;
		return `10.${suffix.substring(0, 1)}.${suffix.substring(1, 2)}.${suffix.substring(2)}`;
	}
	if (compressed.startsWith('S')) {
		const suffix = compressed.substring(1);
		if (suffix.length === 0) return '172.0.0.0';
		if (suffix.length === 1) return `172.${suffix}.0.0`;
		if (suffix.length === 2) return `172.${suffix.substring(0, 1)}.${suffix.substring(1)}.0`;
		return `172.${suffix.substring(0, 1)}.${suffix.substring(1, 2)}.${suffix.substring(2)}`;
	}
	const num = parseInt(compressed, 36);
	return [
		(num >> 24) & 0xFF,
		(num >> 16) & 0xFF,
		(num >> 8) & 0xFF,
		num & 0xFF
	].join('.');
}

function compressNumber(num) {
	return num.toString(36).toUpperCase();
}

function decompressNumber(compressed) {
	return parseInt(compressed, 36);
}

async function validateSDPWithPeerConnection(sdp) {
	return new Promise(async (resolve, reject) => {
		try {
			const pc = new RTCPeerConnection({
				iceServers: [{
					urls: 'stun:stun.l.google.com:19302'
				}]
			});
			const type = sdp.includes('a=setup:actpass') ? 'offer' : 'answer';
			const timeout = setTimeout(() => {
				pc.close();
				reject(new Error("SDP validation timed out"));
			}, 5000);
			pc.onicegatheringstatechange = () => {
				if (pc.iceGatheringState === 'complete') {
					clearTimeout(timeout);
					pc.close();
					resolve(true);
				}
			};
			pc.onicecandidateerror = (event) => {
				console.warn("ICE candidate error:", event);
			};
			pc.oniceconnectionstatechange = () => {
				if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'disconnected') {
					clearTimeout(timeout);
					pc.close();
					resolve(false);
				}
			};
			try {
				if (type === 'offer') {
					await pc.setRemoteDescription({
						type,
						sdp
					});
					const answer = await pc.createAnswer();
					await pc.setLocalDescription(answer);
				} else {
					pc.createDataChannel('validate');
					const offer = await pc.createOffer();
					await pc.setLocalDescription(offer);
					await pc.setRemoteDescription({
						type,
						sdp
					});
				}
				clearTimeout(timeout);
				pc.close();
				resolve(true);
			} catch (err) {
				clearTimeout(timeout);
				pc.close();
				console.error("Error validating SDP:", err);
				reject(err);
			}
		} catch (err) {
			reject(err);
		}
	});
}

function priorityToCompact(priority) {
	return priority.toString(36);
}

function compactToPriority(compact) {
	return parseInt(compact, 36);
}

function hexToCompact(hex) {
	const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
	let result = '';
	for (let i = 0; i < hex.length; i += 6) {
		const chunk = hex.substr(i, 6);
		if (chunk.length < 6) {
			const value = parseInt(chunk, 16);
			result += chars[value % 64];
			if (chunk.length > 2) {
				result += chars[Math.floor(value / 64) % 64];
			}
		} else {
			const value = parseInt(chunk, 16);
			result += chars[value & 0x3F];
			result += chars[(value >> 6) & 0x3F];
			result += chars[(value >> 12) & 0x3F];
			result += chars[(value >> 18) & 0x3F];
		}
	}
	return result;
}

function compactToHex(compact) {
	const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
	let result = '';
	for (let i = 0; i < compact.length; i += 4) {
		let value = 0;
		for (let j = 0; j < 4 && i + j < compact.length; j++) {
			const char = compact[i + j];
			const charValue = chars.indexOf(char);
			if (charValue === -1) {
				throw new Error(`Invalid character in compact string: ${char}`);
			}
			value |= charValue << (j * 6);
		}
		const hex = value.toString(16).padStart(6, '0');
		result += hex;
	}
	// Ensure result is exactly 64 characters (32 bytes)
	return result.substring(0, 64);
}

async function testSample(name, sdp) {
	const logOutput = document.getElementById('testOutput');
	logOutput.innerHTML += `\n--- Testing ${name} ---\n`;
	try {
		logOutput.innerHTML += `##### Original SDP:\n${sdp}\n`;
		logOutput.innerHTML += `##### Original SDP length: ${sdp.length} bytes\n`;
		const compressed = compressSDP(sdp);
		logOutput.innerHTML += `##### Compressed SDP:\n${compressed}\n`;
		logOutput.innerHTML += `##### Compressed length: ${compressed.length} bytes\n`;
		logOutput.innerHTML += `##### Compression ratio: ${Math.round(compressed.length / sdp.length * 100)}%\n`;
		const decompressed = decompressSDP(compressed);
		logOutput.innerHTML += `##### Decompression successful: ${decompressed.length > 0}\n`;
		logOutput.innerHTML += `##### Decompressed SDP:\n${decompressed}\n`;
		logOutput.innerHTML += `##### Testing decompressed SDP with new peer connection...\n`;
		try {
			const valid = await validateSDPWithPeerConnection(decompressed);
			logOutput.innerHTML += `SDP validation: ${valid ? "PASSED" : "FAILED"}\n`;
			if (!valid) {
				throw new Error(`SDP validation failed for ${name}`);
			}
		} catch (err) {
			logOutput.innerHTML += `SDP validation error: ${err.message}\n`;
		}
		const qrCompressed = compressSDPForQR(sdp);
		logOutput.innerHTML += `QR Compressed length: ${qrCompressed.length} bytes\n`;
		logOutput.innerHTML += `QR Compression ratio: ${Math.round(qrCompressed.length / sdp.length * 100)}%\n`;
		const qrDecompressed = decompressSDPFromQR(qrCompressed);
		logOutput.innerHTML += `QR Decompression successful: ${qrDecompressed.length > 0}\n`;
		return true;
	} catch (err) {
		logOutput.innerHTML += `Error testing ${name}: ${err.message}\n`;
		throw err;
	}
}

async function collectSDPSamples() {
	const samples = {
		offer: {
			withCandidates: null,
			withoutCandidates: null
		},
		answer: {
			withCandidates: null,
			withoutCandidates: null
		}
	};

	const logOutput = document.getElementById('testOutput');
	logOutput.innerHTML += "Collecting SDP samples...\n";

	const pc1 = new RTCPeerConnection({
		iceServers: [{
			urls: 'stun:stun.l.google.com:19302'
		}]
	});
	const pc2 = new RTCPeerConnection({
		iceServers: [{
			urls: 'stun:stun.l.google.com:19302'
		}]
	});

	const dataChannel = pc1.createDataChannel('data');
	const offer = await pc1.createOffer();
	samples.offer.withoutCandidates = offer.sdp;
	await pc1.setLocalDescription(offer);
	await pc2.setRemoteDescription(offer);
	const answer = await pc2.createAnswer();
	samples.answer.withoutCandidates = answer.sdp;
	await pc2.setLocalDescription(answer);
	await pc1.setRemoteDescription(answer);

	logOutput.innerHTML += "Waiting for ICE gathering to complete...\n";
	await Promise.all([
		waitForIceGathering(pc1),
		waitForIceGathering(pc2)
	]);

	samples.offer.withCandidates = pc1.localDescription.sdp;
	samples.answer.withCandidates = pc2.localDescription.sdp;
	logOutput.innerHTML += "SDP samples collected successfully.\n";

	dataChannel.close();
	pc1.close();
	pc2.close();
	return samples;
}


function compressSDPForQR(sdp) {
          const iceUfrag = sdp.match(/a=ice-ufrag:([^\r\n]+)/)[1];
          const icePwd = sdp.match(/a=ice-pwd:([^\r\n]+)/)[1];
          const fingerprintMatch = sdp.match(/a=fingerprint:sha-256 ([^\r\n]+)/);
          const fingerprint = fingerprintMatch[1].replace(/:/g, '');
          const isOffer = sdp.match(/a=setup:actpass/) !== null;
          const candidates = [];
          const candidateRegex = /a=candidate:([^\r\n]+)/g;
          let candidateMatch;
          while ((candidateMatch = candidateRegex.exec(sdp)) !== null) {
            const parts = candidateMatch[1].split(' ');
            if (parts[4].includes(':')) continue;
            const foundation = parts[0];
            const component = parts[1];
            const protocol = parts[2].toLowerCase() === 'udp' ? '1' : '2';
            const priority = parseInt(parts[3]);
            const ip = compressIP(parts[4]);
            const port = parseInt(parts[5]);
            let typeCode;
            switch(parts[7]) {
              case 'host': typeCode = '1'; break;
              case 'srflx': typeCode = '2'; break;
              case 'relay': typeCode = '3'; break;
              default: typeCode = '0';
            }
            let relInfo = '';
            const rAddrIndex = parts.indexOf('raddr');
            const rPortIndex = parts.indexOf('rport');
            if (rAddrIndex !== -1 && rPortIndex !== -1 &&
                rAddrIndex + 1 < parts.length && rPortIndex + 1 < parts.length) {
              relInfo = compressIP(parts[rAddrIndex + 1]) + parts[rPortIndex + 1];
            }
            candidates.push(`${foundation}${component}${protocol}${compressNumber(priority)}${ip}${port}${typeCode}${relInfo}`);
          }
          const qrFingerprint = fingerprintToQR(fingerprint);
          let result = `Q${isOffer ? '1' : '0'}${iceUfrag}~${icePwd}~${qrFingerprint}`;
          if (candidates.length > 0) {
            result += `~${candidates.join(',')}`;
          }
          return result;
        }

        function decompressSDPFromQR(compressed) {
          if (!compressed.startsWith('Q')) {
            throw new Error('Unsupported QR compression format');
          }
          const isOffer = compressed[1] === '1';
          const parts = compressed.substring(2).split('~');
          const iceUfrag = parts[0];
          const icePwd = parts[1];
          const fingerprint = qrToFingerprint(parts[2]);
          const formattedFingerprint = fingerprint.match(/.{2}/g).join(':');
          let sdp = [
            'v=0',
            'o=- ' + Math.floor(Date.now()) + ' 2 IN IP4 127.0.0.1',
            's=-',
            't=0 0',
            'a=group:BUNDLE 0',
            'a=extmap-allow-mixed',
            'a=msid-semantic: WMS',
            'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
            'c=IN IP4 0.0.0.0',
            `a=ice-ufrag:${iceUfrag}`,
            `a=ice-pwd:${icePwd}`,
            'a=ice-options:trickle',
            `a=fingerprint:sha-256 ${formattedFingerprint}`,
            `a=setup:${isOffer ? 'actpass' : 'active'}`,
            'a=mid:0'
          ].join('\r\n');
          if (parts.length > 3 && parts[3]) {
            const candidates = parts[3].split(',');
            let candidateLines = '';
            for (const candidate of candidates) {
              let i = 0;
              let j = i;
              while (j < candidate.length && /^\d$/.test(candidate[j])) j++;
              const foundation = candidate.substring(i, j);
              i = j;
              if (i >= candidate.length) continue;
              const component = candidate.substring(i, i + 1);
              i += 1;
              if (i >= candidate.length) continue;
              const protocolCode = candidate.substring(i, i + 1);
              const protocol = protocolCode === '1' ? 'UDP' : 'TCP';
              i += 1;
              if (i >= candidate.length) continue;
              j = i;
              while (j < candidate.length && /^[0-9A-Z]$/.test(candidate[j])) j++;
              const priorityEncoded = candidate.substring(i, j);
              const priority = decompressNumber(priorityEncoded);
              i = j;
              if (i >= candidate.length) continue;
              j = i;
              while (j < candidate.length && !/^\d$/.test(candidate[j]) && candidate[j] !== '1' &&
                     candidate[j] !== '2' && candidate[j] !== '3' && candidate[j] !== '0') j++;
              const ipEncoded = candidate.substring(i, j);
              const ip = decompressIP(ipEncoded);
              i = j;
              if (i >= candidate.length) continue;
              j = i;
              while (j < candidate.length && /^\d$/.test(candidate[j])) j++;
              const port = candidate.substring(i, j);
              i = j;
              if (i >= candidate.length) continue;
              const typeCode = candidate.substring(i, i + 1);
              i += 1;
              let type;
              switch(typeCode) {
                case '1': type = 'host'; break;
                case '2': type = 'srflx'; break;
                case '3': type = 'relay'; break;
                default: type = 'unknown';
              }
              candidateLines += `\r\na=candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`;
              if (i < candidate.length && (type === 'srflx' || type === 'relay')) {
                j = i;
                while (j < candidate.length && !/^\d$/.test(candidate[j])) j++;
                const relatedIPEncoded = candidate.substring(i, j);
                const relatedIP = decompressIP(relatedIPEncoded);
                i = j;
                j = i;
                while (j < candidate.length && /^\d$/.test(candidate[j])) j++;
                const relatedPort = candidate.substring(i, j);
                if (relatedIP && relatedPort) {
                  candidateLines += ` raddr ${relatedIP} rport ${relatedPort}`;
                }
              }
              if (protocol === 'TCP') {
                candidateLines += ' tcptype active';
              }
            }
            sdp += candidateLines;
            if (candidateLines) {
              sdp += '\r\na=end-of-candidates';
            }
          }
          return sdp;
        }


        function fingerprintToQR(hex) {
          let result = '';
          for (let i = 0; i < hex.length; i += 4) {
            const chunk = hex.substr(i, 4);
            if (chunk.length < 4) {
              const value = parseInt(chunk, 16);
              result += value.toString(36).toUpperCase().padStart(3, '0').substring(0, 3);
            } else {
              const value = parseInt(chunk, 16);
              result += value.toString(36).toUpperCase().padStart(3, '0').substring(0, 3);
            }
          }
          return result;
        }

        function qrToFingerprint(qrFp) {
          let result = '';
          for (let i = 0; i < qrFp.length; i += 3) {
            const chunk = qrFp.substr(i, 3);
            const value = parseInt(chunk, 36);
            result += value.toString(16).padStart(4, '0').substring(0, 4);
          }
          return result.padEnd(64, '0');
        }

        function sdpToQRCode(sdp, element) {
          const compressed = compressSDPForQR(sdp);
          new QRCode(element, {
            text: compressed,
            width: 128,
            height: 128,
            colorDark: "#000000",
            colorLight: "#ffffff",
            correctLevel: QRCode.CorrectLevel.M
          });
          return compressed;
        }

        function compressICECandidates(candidates) {
            try {
                if (!candidates || !candidates.length) return "";
                const hostCandidates = candidates
                    .filter(c => c.candidate && c.candidate.includes('typ host') && !c.candidate.includes(':'))
                    .slice(0, 2);
                if (!hostCandidates.length) return "";
                const compressed = hostCandidates.map(c => {
                    const parts = c.candidate.split(' ');
                    return `${parts[0].replace('candidate:', '')},${parts[1]},${parts[2].toLowerCase() === 'udp' ? 'u' : 't'},${parts[4]},${parts[5]},h`;
                });
                return compressed.join('/');
            } catch (e) {
                console.error("Error compressing ICE candidates:", e);
                return "";
            }
        }

        function decompressICECandidates(compressed) {
            try {
                if (!compressed) return [];
                const candidates = compressed.split('/').map(c => {
                    const parts = c.split(',');
                    if (parts.length < 6) return null;
                    const foundation = parts[0];
                    const component = parts[1];
                    const protocol = parts[2] === 'u' ? 'UDP' : 'TCP';
                    const ip = parts[3];
                    const port = parts[4];
                    const type = parts[5] === 'h' ? 'host' :
                               (parts[5] === 's' ? 'srflx' :
                               (parts[5] === 'r' ? 'relay' : 'host'));
                    const priority = type === 'host' ? 2122260223 :
                                   (type === 'srflx' ? 1686052607 :
                                   (type === 'relay' ? 1685987071 : 1684797951));
                    return {
                        candidate: `candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`,
                        sdpMid: "0",
                        sdpMLineIndex: 0,
                        usernameFragment: null
                    };
                }).filter(c => c !== null);
                return candidates;
            } catch (e) {
                console.error("Error decompressing ICE candidates:", e);
                return [];
            }
        }


        function waitForIceGathering(pc) {
          return new Promise(resolve => {
            if (pc.iceGatheringState === 'complete') {
              resolve();
              return;
            }
            const checkState = () => {
              if (pc.iceGatheringState === 'complete') {
                pc.removeEventListener('icegatheringstatechange', checkState);
                resolve();
              }
            };
            pc.addEventListener('icegatheringstatechange', checkState);
            setTimeout(resolve, 5000);
          });
        }

        async function runAllTests() {
          const logOutput = document.getElementById('testOutput');
          logOutput.innerHTML = "=== SDP COMPRESSION/DECOMPRESSION TEST TOOL ===\n";
          logOutput.innerHTML += "Generating and testing various SDP types...\n";
          
          try {
            const samples = await collectSDPSamples();
            await testSample("Offer with candidates", samples.offer.withCandidates);
            await testSample("Offer without candidates", samples.offer.withoutCandidates);
            await testSample("Answer with candidates", samples.answer.withCandidates);
            await testSample("Answer without candidates", samples.answer.withoutCandidates);
            logOutput.innerHTML += "\n=== ALL TESTS COMPLETED SUCCESSFULLY ===\n";
          } catch (err) {
            logOutput.innerHTML += `Test failed: ${err.message}\n`;
            console.error("Test failed:", err);
          }
        }

        // When the DOM is loaded, set up event handlers
        document.addEventListener('DOMContentLoaded', () => {
          // Tab switching
          document.querySelectorAll('.tab').forEach(tab => {
            tab.addEventListener('click', () => {
              const tabId = tab.getAttribute('data-tab');
              
              // Update active tab
              document.querySelectorAll('.tab').forEach(t => {
                t.classList.remove('active');
              });
              tab.classList.add('active');
              
              // Update active content
              document.querySelectorAll('.tab-content').forEach(content => {
                content.classList.remove('active');
              });
              document.getElementById(tabId).classList.add('active');
            });
          });
          
          // SDP Compression Tab
          document.getElementById('compressBtn').addEventListener('click', () => {
            const sdpInput = document.getElementById('sdpInput').value;
            try {
              const compressed = compressSDP(sdpInput);
              document.getElementById('compressedOutput').value = compressed;
              document.getElementById('originalSize').textContent = sdpInput.length;
              document.getElementById('compressedSize').textContent = compressed.length;
              document.getElementById('compressionRatio').textContent = 
                Math.round(compressed.length / sdpInput.length * 100);
              // Clear any previous QR code
              document.getElementById('qrcode').innerHTML = '';
            } catch (err) {
              alert('Error compressing SDP: ' + err.message);
              console.error('Error compressing SDP:', err);
            }
          });
          
          document.getElementById('compressQRBtn').addEventListener('click', () => {
            const sdpInput = document.getElementById('sdpInput').value;
            try {
              const compressed = compressSDPForQR(sdpInput);
              document.getElementById('compressedOutput').value = compressed;
              document.getElementById('originalSize').textContent = sdpInput.length;
              document.getElementById('compressedSize').textContent = compressed.length;
              document.getElementById('compressionRatio').textContent = 
                Math.round(compressed.length / sdpInput.length * 100);
              
              // Generate QR code
              document.getElementById('qrcode').innerHTML = '';
              new QRCode(document.getElementById('qrcode'), {
                text: compressed,
                width: 200,
                height: 200,
                colorDark: "#000000",
                colorLight: "#ffffff",
                correctLevel: QRCode.CorrectLevel.M
              });
            } catch (err) {
              alert('Error compressing SDP for QR: ' + err.message);
              console.error('Error compressing SDP for QR:', err);
            }
          });
          
          document.getElementById('decompressBtn').addEventListener('click', () => {
            const compressedSDP = document.getElementById('compressedOutput').value;
            try {
              const decompressed = decompressSDP(compressedSDP);
              document.getElementById('decompressedOutput').value = decompressed;
            } catch (err) {
              alert('Error decompressing SDP: ' + err.message);
              console.error('Error decompressing SDP:', err);
            }
          });
          
          document.getElementById('decompressQRBtn').addEventListener('click', () => {
            const compressedSDP = document.getElementById('compressedOutput').value;
            try {
              const decompressed = decompressSDPFromQR(compressedSDP);
              document.getElementById('decompressedOutput').value = decompressed;
            } catch (err) {
              alert('Error decompressing QR SDP: ' + err.message);
              console.error('Error decompressing QR SDP:', err);
            }
          });
          
          document.getElementById('copyCompressedBtn').addEventListener('click', () => {
            const compressedOutput = document.getElementById('compressedOutput');
            compressedOutput.select();
            document.execCommand('copy');
            alert('Compressed SDP copied to clipboard');
          });
          
          document.getElementById('generateSampleSDPBtn').addEventListener('click', async () => {
            try {
              const pc = new RTCPeerConnection({
                iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
              });
              pc.createDataChannel('sample');
              const offer = await pc.createOffer();
              await pc.setLocalDescription(offer);
              
              // Wait for a short time to allow some ICE candidates to be generated
              await new Promise(resolve => setTimeout(resolve, 1000));
              
              document.getElementById('sdpInput').value = pc.localDescription.sdp;
              pc.close();
            } catch (err) {
              alert('Error generating sample SDP: ' + err.message);
              console.error('Error generating sample SDP:', err);
            }
          });
          
          // Testing Tab
          document.getElementById('runTestsBtn').addEventListener('click', async () => {
            await runAllTests();
          });
          
          document.getElementById('validateSDPBtn').addEventListener('click', async () => {
            const sdp = document.getElementById('decompressedOutput').value;
            if (!sdp) {
              alert('Please decompress an SDP first');
              return;
            }
            
            const logOutput = document.getElementById('testOutput');
            logOutput.innerHTML = "Validating SDP...\n";
            
            try {
              const valid = await validateSDPWithPeerConnection(sdp);
              logOutput.innerHTML += `SDP validation: ${valid ? "PASSED" : "FAILED"}\n`;
            } catch (err) {
              logOutput.innerHTML += `SDP validation error: ${err.message}\n`;
              console.error('SDP validation error:', err);
            }
          });
          
          // ICE Candidates Tab
          document.getElementById('compressIceBtn').addEventListener('click', () => {
            try {
              const iceInput = document.getElementById('iceInput').value;
              const candidates = JSON.parse(iceInput);
              const compressed = compressICECandidates(candidates);
              document.getElementById('iceCompressedOutput').value = compressed;
            } catch (err) {
              alert('Error compressing ICE candidates: ' + err.message);
              console.error('Error compressing ICE candidates:', err);
            }
          });
          
          document.getElementById('decompressIceBtn').addEventListener('click', () => {
            try {
              const compressedIce = document.getElementById('iceCompressedOutput').value;
              const candidates = decompressICECandidates(compressedIce);
              document.getElementById('iceDecompressedOutput').value = JSON.stringify(candidates, null, 2);
            } catch (err) {
              alert('Error decompressing ICE candidates: ' + err.message);
              console.error('Error decompressing ICE candidates:', err);
            }
          });
          
          document.getElementById('copyCompressedIceBtn').addEventListener('click', () => {
            const compressedOutput = document.getElementById('iceCompressedOutput');
            compressedOutput.select();
            document.execCommand('copy');
            alert('Compressed ICE copied to clipboard');
          });
          
          document.getElementById('generateSampleIceBtn').addEventListener('click', async () => {
            try {
              const pc = new RTCPeerConnection({
                iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
              });
              
              const candidates = [];
              
              pc.onicecandidate = (event) => {
                if (event.candidate) {
                  candidates.push(event.candidate);
                  document.getElementById('iceInput').value = JSON.stringify(candidates, null, 2);
                }
              };
              
              pc.createDataChannel('sample');
              const offer = await pc.createOffer();
              await pc.setLocalDescription(offer);
              
              // Automatically close after 3 seconds
              setTimeout(() => {
                pc.close();
                if (candidates.length === 0) {
                  document.getElementById('iceInput').value = JSON.stringify([{
                    candidate: "candidate:1853936366 1 UDP 2122260223 192.168.1.2 56789 typ host",
                    sdpMid: "0",
                    sdpMLineIndex: 0
                  }], null, 2);
                }
              }, 3000);
            } catch (err) {
              alert('Error generating sample ICE candidates: ' + err.message);
              console.error('Error generating sample ICE candidates:', err);
            }
          });
        });
    </script>
</body>
</html>